Repository navigation
Conversation
`get_subagent_result` reported "Subagent not found" for a background run that had finished moments earlier. Settling deletes the live record, the child's wrapper is released once the child goes idle, and the session file can still be unresolvable right after it settled: with all three gone, nothing answered for an id this process had handed out itself, and the parent had no way to check the claim — it reasonably reads it as the child's session having been deleted. - A settled run moves to `getSettledSubagentRuns()` (64 entries, insertion order) instead of vanishing. `get()` consults it only after a live wrapper and a read of the session file have both failed, so a fresh read of the file always wins and an id that was never run is still reported as unknown. - `resume` re-registers the run live, and a resume that settles again overwrites the record, so the fallback cannot serve a superseded result. - The new test settles a run, then takes away both the wrapper and the path lookup and still expects the completed result. It fails without the fix (node --test and bun test alike) and passes with it.
659a361 to
351e729
Compare
pi creates a session file only once the session holds a user or assistant message. A run that ends before its prompt reaches the model (stopped while queued or connecting MCP servers, a provider without credentials) kept its metadata and result in memory only, so once the idle child's wrapper was released, get_subagent_result and resume answered "Subagent not found" and the child left the sidebar. Write the file at dispatch and reload it with setSessionFile(), so pi appends every later entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the child's session file written at dispatch, the file lookup finds every settled run, so the in-memory record only ever answered for a child whose file is gone: a deleted session. get() then reported it, and resume reopened its missing path as a new session with the default tools in the server's cwd instead of refusing with "Subagent not found". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks @brynne8 for chasing this. The bug is real, but it's narrower than described. A child that reached the model is always found from its file afterwards, even with a cold path cache. What disappears is a child whose run ended before its prompt was sent: stopped while queued, stopped while connecting MCP servers, or refused for missing credentials. pi only creates a session file once a user or assistant message exists, so those children lived only in their wrapper. Once the idle wrapper was released, The settled-run map also opened a worse hole. For a child whose file is gone (never written, or deleted from the sidebar), So I pushed two commits on top of yours:
Both come with integration tests against real AgentSessions; each fails without its commit. On a merge with main, tsc, lint and the full unit suite pass. The PR description still describes the map; I'll leave it as the record of the original report. |
What breaks today
A parent dispatches a background
Agentrun, the child finishes, and the parent's nextget_subagent_result({ sessionId })— for the very id theAgentcall just returned — answers "Subagent not found". The parent has no way to check that claim, so it reasonably concludes the child's session was deleted, and either re-does the work or reports a loss that never happened.Why it happens
Three lookups in
get()all come up empty at once, right after a run settles:getSubagentRuns().delete(...)at the six settle points).AgentSessionWrapperis released once the child goes idle, so there is no live wrapper to read from.dependencies.resolveSessionPath(sessionId)can still miss a session that settled moments ago.With all three gone, nothing answered for an id this process had handed out itself.
The fix
getSettledSubagentRuns()— a bounded map (64 entries, insertion order) — instead of vanishing.get()consults it only after both the live wrapper and the session-file read have failed.What the fallback deliberately cannot do:
getreturnsnull).resumere-registers the run live, and a re-settled resume overwrites the record.Test evidence
The new test is behavioural: it resumes/settles a run against the real controller, then takes away both the wrapper (
getSessionreturns nothing,resolveSessionPathreturnsnull) and assertsget()still returns the completed result with the resumed run's text; it also asserts an id nothing ever ran is stillnull. No source-text assertions.main(test kept, fix stashed):bun test lib/subagent-runtime.test.mjs→14 pass / 1 fail(AssertionError: actual: undefined, expected: "completed")node --experimental-strip-types --test lib/subagent-runtime.test.mjs→ same single failure15 pass / 0 failnpm test:2627 tests, 2626 pass, 0 fail, 1 skippedbun x tsc --noEmit→ exit 0;bun run lint(eslint) → exit 0